package com.google.code.gronono.gps.pattern;

import java.io.File;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TreeMap;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.google.code.gronono.commons.exif.ExifData;
import com.google.code.gronono.commons.i18n.BundleKey;
import com.google.code.gronono.commons.i18n.BundleName;

/**
 * Classe permettant de déterminer un chemin de fichier à partir d'un pattern basé sur des mots-clés.
 * <br/>Les mots-clés sont prédéfinis et non paramétrables (sans cela leur implémentation devrait l'être également).
 * @author gael.lorent-fonfrede
 */
@BundleName(value = "com.google.code.gronono.gps.gui")
public class KeywordsPattern {
	/** Logger. */
	private static final Logger logger = Logger.getLogger(KeywordsPattern.class);

	/** Liste des éléments de pattern supportés. */
	private static List<SupportedPatternElement> keywords = Arrays.asList(SupportedPatternElement.values());

	/** Délimiteur de mot-clés. */
	private static char keywordDelimiter = '%';
	/** Délimiteur de mot-clés (version html escaped). */
	public static String keywordDelimiterForHtml = "&#37;";

	/** Séquence. */
	private static int sequential_nb = 1;

	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.year.descr")
	private static String EXIF_YEAR_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.month.descr")
	private static String EXIF_MONTH_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.day.descr")
	private static String EXIF_DAY_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.hour.descr")
	private static String EXIF_HOUR_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.minutes.descr")
	private static String EXIF_MINUTES_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.exif.seconds.descr")
	private static String EXIF_SECONDS_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.year.descr")
	private static String LASTMOD_YEAR_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.month.descr")
	private static String LASTMOD_MONTH_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.day.descr")
	private static String LASTMOD_DAY_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.hour.descr")
	private static String LASTMOD_HOUR_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.minutes.descr")
	private static String LASTMOD_MINUTES_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.lastmod.seconds.descr")
	private static String LASTMOD_SECONDS_DESCR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.original.dir.descr")
	private static String ORIGINAL_DIR;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.original.file.name.descr")
	private static String ORIGINAL_FILE_NAME;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.original.file.extension.descr")
	private static String ORIGINAL_FILE_EXTENSION;
	/** Description du mot-clé. */
	@BundleKey(value = "keywords.pattern.sequence.descr")
	private static String SEQUENCE;

	/** Message d'erreur si arguments invalides. */
	@BundleKey(value = "keywords.pattern.invalid.arguments.err.msg")
	private static String INVALID_ARGUMENTS_ERR_MSG;
	/** Message d'erreur si pattern invalide. */
	@BundleKey(value = "keywords.pattern.invalid.pattern.err.msg")
	private static String INVALID_PATTERN_ERR_MSG;
	/** Message d'erreur si mot-clé invalide. */
	@BundleKey(value = "keywords.pattern.invalid.keyword.err.msg")
	private static String INVALID_KEYWORD_ERR_MSG;
	/** Message d'erreur si date exif utilisée et nulle. */
	@BundleKey(value = "keywords.pattern.exif.date.null.err.msg")
	private static String EXIF_DATE_NULL_ERR_MSG;
	/** Message d'erreur si date exif utilisée et nulle. */
	@BundleKey(value = "keywords.pattern.lastmod.date.null.err.msg")
	private static String LASTMOD_DATE_NULL_ERR_MSG;

	/**
	 * Fournit une map avec la liste des mots-clés supportés et leur description respective.
	 * @return La liste des mots-clés supportés et leur description respective.
	 */
	public static TreeMap<SupportedPatternElement, String> getSupportedKeywords() {
		final TreeMap<SupportedPatternElement, String> result = new TreeMap<SupportedPatternElement, String>();
		for (final SupportedPatternElement keyword : keywords) {
			String descr = null;
			switch (keyword) {
			case EXIF_YYYY:
				descr = EXIF_YEAR_DESCR;
				break;
			case EXIF_MM:
				descr = EXIF_MONTH_DESCR;
				break;
			case EXIF_DD:
				descr = EXIF_DAY_DESCR;
				break;
			case EXIF_hh:
				descr = EXIF_HOUR_DESCR;
				break;
			case EXIF_mm:
				descr = EXIF_MINUTES_DESCR;
				break;
			case EXIF_ss:
				descr = EXIF_SECONDS_DESCR;
				break;
			case LastMod_YYYY:
				descr = LASTMOD_YEAR_DESCR;
				break;
			case LastMod_MM:
				descr = LASTMOD_MONTH_DESCR;
				break;
			case LastMod_DD:
				descr = LASTMOD_DAY_DESCR;
				break;
			case LastMod_hh:
				descr = LASTMOD_HOUR_DESCR;
				break;
			case LastMod_mm:
				descr = LASTMOD_MINUTES_DESCR;
				break;
			case LastMod_ss:
				descr = LASTMOD_SECONDS_DESCR;
				break;
			case OD:
				descr = MessageFormat.format(ORIGINAL_DIR, "'" + File.separator + "'");
				break;
			case ON:
				descr = ORIGINAL_FILE_NAME;
				break;
			case OE:
				descr = ORIGINAL_FILE_EXTENSION;
				break;
			case S:
				descr = SEQUENCE;
				break;
			default:
				break;
			}
			if (descr != null) result.put(keyword, descr);
		}
		return result;
	}

	/**
	 * Validation du pattern par mots-clés spécifié.
	 * @param pattern Le pattern à valider.
	 * @throws IllegalArgumentException Si le pattern est invalide.
	 */
	public static void validatePattern(final String pattern) throws IllegalArgumentException {
		boolean isValid = true;

		// Validation des arguments
		if (StringUtils.isBlank(pattern)) {
			isValid = false;
			final String msg = INVALID_ARGUMENTS_ERR_MSG;
			logger.error(msg);
			throw new IllegalArgumentException(msg);
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Validation du pattern (" + pattern + ").");
		}

		// Détection du pattern
		StringBuilder sequence = new StringBuilder();
		for (final char c : pattern.toCharArray()) {
			if (isValid && isCharValid(c)) {
				sequence.append(c);
				// Détection de mot-clés
				final SupportedPatternElement key = getKeyword(sequence.toString());
				if (key != null) {
					if (logger.isTraceEnabled()) {
						logger.trace("Pattern détecté : " + key);
					}
					// Raz de la séquence courante
					sequence = new StringBuilder();
				} else if (StringUtils.countMatches(sequence.toString(), String.valueOf(keywordDelimiter)) > 1) {
					isValid = false;
					final String msg = MessageFormat.format(INVALID_KEYWORD_ERR_MSG, sequence.toString());
					logger.error(msg);
					throw new IllegalArgumentException(msg);
				}
			} else if (logger.isDebugEnabled()) {
				logger.debug("Caractère du pattern ignoré (" + c + ").");
			}
		}
	}

	/**
	 * Applique un pattern de transformation à un nom de fichier.
	 *
	 * @param srcFile Le fichier concerné.
	 * @param pattern Le pattern à appliquer.
	 * @param exifData Les métadonnées du fichier.
	 * @return dstFileName Le nom de fichier transformé après interprétation et application du pattern.
	 *
	 * @throws Exception En cas d'erreur lors de l'application du pattern
	 *
	 * @see SupportedPatternElement pour la liste des éléments supportés.
	 */
	public String applyPattern(final File srcFile, final String pattern, final ExifData exifData) throws Exception {
		// Validation des arguments
		if (StringUtils.isBlank(pattern) || (srcFile == null) || (!srcFile.exists()) || (!srcFile.isFile())) {
			final String msg = INVALID_ARGUMENTS_ERR_MSG;
			logger.error(msg);
			throw new IllegalArgumentException(msg);
		}

		// Validation du pattern
		try {
			validatePattern(pattern);
		} catch (final IllegalArgumentException e) {
			final String msg = INVALID_PATTERN_ERR_MSG;
			logger.error(msg);
			throw new IllegalArgumentException(msg);
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Application du pattern (" + pattern + ") au fichier (" + srcFile.getName() + ").");
		}

		// Application du pattern
		final StringBuilder targetFileName = new StringBuilder();
		final StringBuilder sequence = new StringBuilder();
		for (final char c : pattern.toCharArray()) {
			if (isCharValid(c)) {
				sequence.append(c);
				// Détection de mot-clés
				final SupportedPatternElement key = getKeyword(sequence.toString());
				if (key != null) {
					if (logger.isTraceEnabled()) {
						logger.trace("Pattern détecté : " + key);
					}
					// Conversion de la séquence courante
					final String convertedSequence = applyPattern(sequence.toString(), key, srcFile, exifData);
					targetFileName.append(convertedSequence);
					if (logger.isTraceEnabled()) {
						logger.trace("Conversion appliquée : " + convertedSequence);
					}
					// Raz de la séquence courante
					sequence.delete(0, sequence.length());
				}
			} else if (logger.isDebugEnabled()) {
				logger.debug("Caractère du pattern (" + c + ") non-pris en compte.");
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Nom de fichier converti : (" + targetFileName.toString() + ").");
		}
		return targetFileName.toString();
	}

	/**
	 * Détermine si le caractère est autorisé.
	 * @param c Le caractère à tester.
	 *
	 * @return <code>true</code> si le caractère est à prendre en compte, <code>false</code> s'il est à ignorer.
	 */
	private static boolean isCharValid(final char c) {
		boolean isValid = false;
		isValid = (StringUtils.isAlphanumericSpace(String.valueOf(c))
				|| (keywordDelimiter == c)
				|| (File.separatorChar == c)|| ('/' == c)
				|| ('-' == c) || ('_' == c) || ('.' == c));
		return isValid;
	}

	/**
	 * Détecte si la séquence en cours contient un élément de pattern supporté.
	 * @param sequence La séquence à tester.
	 *
	 * @return Le mot-clé trouvé, ou <code>null</code> sinon.
	 */
	private static SupportedPatternElement getKeyword(final String sequence) {
		for (final SupportedPatternElement pattern : keywords) {
			if (StringUtils.contains(sequence, keywordDelimiter + pattern.name() + keywordDelimiter)) {
				return pattern;
			}
		}
		return null;
	}

	/**
	 * Applique le pattern à une séquence.
	 * @param sequence La séquence à traiter.
	 * @param key Le mot-clé à utiliser.
	 * @param srcFile Le fichier concerné.
	 * @param exifData Les métadonnées Exif du fichier (peut-être <code>null</code>).
	 * @return La séquence interprétée.
	 * @throws Exception Si la date d'Exif n'existe pas.
	 */
	private String applyPattern(String sequence, SupportedPatternElement key, final File srcFile, final ExifData exifData) throws Exception {
		String result = null;
		final StringBuilder tmp = new StringBuilder();
		// Si la clé fournie est nulle on la récupère
		if (key == null) {
			key = getKeyword(sequence);
		}
		if (key != null) {
			// Filtrage de la séquence
			sequence = StringUtils.replace(sequence, String.valueOf(keywordDelimiter), "");
			// On prend en compte ce qui peut éventuellement 'dépasser' à gauche de la séquence
			if (sequence.length() > key.name().length()) {
				tmp.append(StringUtils.left(sequence, sequence.length() - key.name().length()));
			}
			// Récupération des infos
			Date exifDate = null;
			if (exifData != null) {
				exifDate = exifData.getDate();
			}
			final Date lastModDate = new Date(srcFile.lastModified());
			String srcDir = "";
			if (srcFile.getParentFile() != null) {
				srcDir = StringUtils.substringAfter(srcFile.getParentFile().getPath(), File.separator);
			}
			final String srcFileName = FilenameUtils.getBaseName(srcFile.getName());
			final String srcFileExt = FilenameUtils.getExtension(srcFile.getName());
			// Conversion du mot-clé
			switch (key) {
			case EXIF_YYYY:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("yyyy").format(exifDate));
				break;
			case EXIF_MM:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("MM").format(exifDate));
				break;
			case EXIF_DD:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("dd").format(exifDate));
				break;
			case EXIF_hh:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("hh").format(exifDate));
				break;
			case EXIF_mm:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("mm").format(exifDate));
				break;
			case EXIF_ss:
				if (exifDate == null) {
					throw new Exception(EXIF_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("ss").format(exifDate));
				break;
			case LastMod_YYYY:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("yyyy").format(lastModDate));
				break;
			case LastMod_MM:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("MM").format(lastModDate));
				break;
			case LastMod_DD:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("dd").format(lastModDate));
				break;
			case LastMod_hh:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("hh").format(lastModDate));
				break;
			case LastMod_mm:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("mm").format(lastModDate));
				break;
			case LastMod_ss:
				if (lastModDate == null) {
					throw new Exception(LASTMOD_DATE_NULL_ERR_MSG);
				}
				tmp.append(new SimpleDateFormat("ss").format(lastModDate));
				break;
			case OD:
				tmp.append(srcDir);
				break;
			case ON:
				tmp.append(srcFileName);
				break;
			case OE:
				tmp.append(srcFileExt);
				break;
			case S:
				tmp.append(sequential_nb);
				sequential_nb++;
				break;
			case SS:
				if (sequential_nb < 10) tmp.append(StringUtils.leftPad(String.valueOf(sequential_nb), 2, "0"));
				else tmp.append(sequential_nb);
				sequential_nb++;
				break;
			case SSS:
				if (sequential_nb < 100) tmp.append(StringUtils.leftPad(String.valueOf(sequential_nb), 3, "0"));
				else tmp.append(sequential_nb);
				sequential_nb++;
				break;
			case SSSS:
				if (sequential_nb < 1000) tmp.append(StringUtils.leftPad(String.valueOf(sequential_nb), 4, "0"));
				else tmp.append(sequential_nb);
				sequential_nb++;
				break;
			case SSSSS:
				if (sequential_nb < 10000) tmp.append(StringUtils.leftPad(String.valueOf(sequential_nb), 5, "0"));
				else tmp.append(sequential_nb);
				sequential_nb++;
				break;
			default:
				break;
			}
		}
		if (tmp.length() > 0) {
			result = tmp.toString();
		}
		return result;
	}

	/**
	 * Réinitialisation de l'état de l'instance.
	 * <li>Remise à 1 de la séquence.
	 */
	public void reset() {
		KeywordsPattern.sequential_nb = 1;
	}
}
